# ~*~ coding: utf-8 ~*~
import time
import os
import socket
import traceback

from fabric2 import Connection
from invoke.config import Config

from flask import current_app
from flask_login import current_user

from apps.codes.service.repo.git_ import Git
from apps.codes.service.repo.svn_ import SVN
from apps.codes.service.utils import mkdir
from apps.extensions import db
from apps.assets.models import Asset, AdminUser
from apps.codes.models import Project, Task, Server, Filter
from apps.codes.service.models import WSProxy
from apps.codes.service.connection import SSHConnection


class Deployer(object):
    sequence = 0

    stage = 'ready'
    stage_start = 'deploy_start'
    stage_prev_deploy = 'prev_deploy'
    stage_checkout = 'checkout'
    stage_post_deploy = 'post_deploy'

    stage_prev_release = 'prev_release'
    stage_release = 'release'
    stage_post_release = 'post_release'
    stage_end = 'deploy_end'

    # 消息发送的类型
    message_type_step = 'step'
    message_type_info = 'info'
    message_type_error = 'error'

    def __init__(self, project_id=None, task_id=None):
        if project_id:
            self.project = db.session.query(Project).get(project_id)
            self.room = project_id
        if task_id:
            self.task = db.session.query(Task).get(task_id)
            self.project = db.session.query(Project).get(self.task.project_id)
            self.room = task_id

            # 发布代码相关
            self.release_version = None
            self.release_version_tgz = None
            self.previous_release_version = None
            self.servers = None
            self.rollback_task = None

        self.local_codebase = current_app.config.get('CODE_BASE').rstrip('/')
        self.local_codebase_project = os.path.join(self.local_codebase, self.project.id)
        mkdir(self.local_codebase_project)
        if self.project.repo_type == Project.REPO_TYPE_GIT:
            self.repo = Git(self.project.repo_url)
        elif self.project.repo_type == Project.REPO_TYPE_SVN:
            self.repo = SVN(self.project.repo_url, self.project.repo_username, self.project.repo_password)
        self.wsporxy = WSProxy.get_wsproxy(self.room)

    def init_release(self, is_rollback=False):
        asset_ids = db.session.query(Server.asset_id).filter(Server.project_id == self.project.id).all()
        self.servers = db.session.query(Asset.manage_ip, Asset.port, Asset.hostname, AdminUser.username, AdminUser.password). \
            join(AdminUser, Asset.admin_user_id == AdminUser.id). \
            filter(Asset.id.in_(list(zip(*asset_ids))[0])). \
            all()

        self.release_version = self.task.pre_link_id if is_rollback else \
            '{project_id}_{task_id}_{timestamp}'.format(
                project_id=self.project.short_id,
                task_id=self.task.short_id,
                timestamp=time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())),
            )

        self.release_version_tgz = '%s.tgz' % self.release_version
        self.previous_release_version = ''

    @staticmethod
    def config():
        cfg = Config(overrides={'run': {'pty': True, 'warn': True, 'hide': True}})
        return cfg

    def send_message(self, tp, data=None):
        if self.wsporxy:
            if tp == self.message_type_step:
                self.wsporxy.send(tp, {'sequence': self.sequence, 'stage': self.stage})
            elif tp == self.message_type_info:
                self.wsporxy.send(tp, data)
            elif tp == self.message_type_error:
                self.wsporxy.send(tp, data)
            else:
                self.wsporxy.send('close', data)

    # ===================== fabric ================
    # SocketHandler

    def prev_deploy(self, conn):
        """
        # TODO
        socketio.sleep(0.001)
        1.代码检出前要做的基础工作
        - 检查 当前用户
        - 检查 python 版本
        - 检查 git 版本
        - 检查 目录是否存在
        - 用户自定义命令

        :return:
        """
        self.stage = self.stage_prev_deploy
        self.sequence = 2
        self.send_message(self.message_type_step)

        # 用户自定义命令
        commands = self.project.prev_deploy
        if commands:
            for command in commands.split('\n'):
                if command.strip().startswith('#') or not command.strip():
                    continue

                with conn.cd(self.local_codebase_project):
                    result = conn.local(command)
                    self.send_message('info', {
                        'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
                    })
                    self.send_message('info', {
                        'message': result.stdout
                    })

    def checkout(self, conn):
        """
        2.检出代码
        :return:
        """
        self.stage = self.stage_checkout
        self.sequence = 3
        self.send_message(self.message_type_step)

        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host,
                                                           command='cd {}'.format(self.local_codebase_project))
        })
        with conn.cd(self.local_codebase_project):
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host,
                                                               command='- checkout code -')
            })
            release_version_path = os.path.join(self.local_codebase_project, self.release_version)
            mkdir(release_version_path)
            # 更新到指定 branch/commit_id 或 tag
            clone = True
            if self.project.repo_type == Project.REPO_TYPE_SVN:
                clone = False
            if self.project.repo_mode == Project.REPO_MODE_BRANCH:
                if self.task.branch == 'trunk':
                    branch = None
                else:
                    branch = self.task.branch
                self.repo.init(release_version_path, is_need_clone=clone)
                self.repo.checkout_2_commit(branch=branch, commit=self.task.commit)
            else:
                self.repo.init(release_version_path, is_need_clone=clone)
                self.repo.checkout_2_tag(tag=self.task.tag)

    def post_deploy(self, conn):
        """
        3.检出代码后要做的任务
        - 用户自定义操作命令
        - 代码编译
        - 清除日志文件及无用文件
        -
        - 压缩打包
        - 传送到版本库 release
        :return:
        """
        self.stage = self.stage_post_deploy
        self.sequence = 4
        self.send_message(self.message_type_step)

        # 用户自定义命令
        commands = self.project.post_deploy
        if commands:
            for command in commands.split('\n'):
                if command.strip().startswith('#') or not command.strip():
                    continue

                local_project_version_path = os.path.join(self.local_codebase_project, self.release_version)
                self.send_message('info', {
                    'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                                   format(local_project_version_path))
                })
                with conn.cd(local_project_version_path):
                    self.send_message('info', {
                        'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
                    })
                    result = conn.local(command)
                    self.send_message('info', {
                        'message': result.stdout
                    })

        # 压缩打包
        # 排除文件发布
        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                           format(self.local_codebase_project))
        })
        with conn.cd(self.local_codebase_project):
            filters = db.session.query(Filter).filter(Filter.project_id == self.project.id).all()
            for ft in filters:
                filename = ft.filename.lstrip('/')

                need_filter_full_path = os.path.join(
                    self.local_codebase_project,
                    self.release_version,
                    filename
                )
                # 文件存在就清空
                if os.path.exists(need_filter_full_path) and os.path.isfile(need_filter_full_path):
                    command = '> {0}'.format(need_filter_full_path)
                    self.send_message('info', {
                        'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
                    })
                    result = conn.local(command)
                    self.send_message('info', {
                        'message': result.stdout
                    })

                # 目录不存在就创建
                need_filter_base_path = '/'.join(need_filter_full_path.split('/')[0:-1])
                if not os.path.exists(need_filter_base_path):
                    os.makedirs(need_filter_base_path)

                # 修该文件内容
                with open(need_filter_full_path, 'w+') as f:
                    f.write(ft.content)

            # 只上线指定目录
            if self.project.sparse_dir:
                command = 'tar zcf {0} -C {1} {2}'.format(
                    self.release_version_tgz,
                    os.path.join(self.release_version,  self.project.sparse_dir),
                    '.'
                )
            else:
                command = 'tar zcf {0} -C {1} {2}'.format(
                    self.release_version_tgz,
                    self.release_version,
                    '.'
                )

            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            result = conn.local(command)
            self.send_message('info', {
                'message': result.stdout
            })

    def prev_release(self, conn):
        """
        4.部署代码到目标机器前做的任务
        - 检查 webroot 父目录是否存在
        :return:
        """
        self.stage = self.stage_prev_release
        self.sequence = 5
        self.send_message(self.message_type_step)

        # 检查 target_releases 父目录是否存在
        command = 'mkdir -p %s' % self.project.target_releases
        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
        })
        result = conn.run(command)
        self.send_message('info', {
            'message': result.stdout
        })

        # 传送到版本库 release
        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host,
                                                           command='- copy local code to remote host -')
        })
        conn.put(
            os.path.join(self.local_codebase_project, self.release_version_tgz),
            remote=self.project.target_releases
        )

        # 解压
        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                           format(self.project.target_releases))
        })
        with conn.cd(self.project.target_releases):
            command = 'mkdir {1} && tar zxf {0} -C {1}'.format(self.release_version_tgz, self.release_version)
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            result = conn.run(command)
            self.send_message('info', {
                'message': result.stdout
            })

        # 用户自定义命令
        target_project_release_path = "%s/%s" % (self.project.target_releases, self.release_version)
        commands = self.project.prev_release
        if commands:
            for command in commands.split('\n'):
                if command.strip().startswith('#') or not command.strip():
                    continue

                self.send_message('info', {
                    'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                                   format(target_project_release_path))
                })
                with conn.cd(target_project_release_path):
                    self.send_message('info', {
                        'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
                    })
                    result = conn.run(command)
                    self.send_message('info', {
                        'message': result.stdout
                    })

        # 需要程序运行用户权限
        if self.project.privilege_user:
            command = 'chown -R {0}:{0} {1}'.format(self.project.privilege_user, target_project_release_path)
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            result = conn.run(command)
            self.send_message('info', {
                'message': result.stdout
            })

    def release(self, conn):
        """
        5.切换代码
        - 上线新版本
        :return:
        """
        self.stage = self.stage_release
        self.sequence = 6
        self.send_message(self.message_type_step)

        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                           format(self.project.target_releases))
        })
        with conn.cd(self.project.target_releases):
            # 1. get previous link
            command = '[ -L %s ] && readlink %s || echo ""' % (self.project.target_root, self.project.target_root)
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            result = conn.run(command)
            self.previous_release_version = os.path.basename(result.stdout).strip()
            self.send_message('info', {
                'message': result.stdout
            })

            # 2. create a tmp link dir
            current_link_tmp_dir = 'current-tmp-%s' % self.task.id
            command = 'ln -sfn {library}/{version} {library}/{current_tmp}'.format(
                library=self.project.target_releases, version=self.release_version,
                current_tmp=current_link_tmp_dir)
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            conn.run(command)
            self.send_message('info', {
                'message': result.stdout
            })

            # 3. move tmp link to webroot
            current_link_tmp_dir = '%s/current-tmp-%s' % (self.project.target_releases, self.task.id)
            command = 'mv -fT %s %s' % (current_link_tmp_dir, self.project.target_root)
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            conn.run(command)
            self.send_message('info', {
                'message': result.stdout
            })

    def post_release(self, conn):
        """
        6.部署代码到目标机器后要做的任务
        - 切换软链
        - 重启 nginx
        :return:
        """
        self.stage = self.stage_post_release
        self.sequence = 7
        self.send_message(self.message_type_step)

        # 用户自定义命令
        commands = self.project.post_release
        if commands:
            for command in commands.split('\n'):
                if command.strip().startswith('#') or not command.strip():
                    continue

                self.send_message('info', {
                    'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                                   format(self.project.target_root))
                })
                with conn.cd(self.project.target_root):
                    self.send_message('info', {
                        'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
                    })
                    result = conn.run(command)
                    self.send_message('info', {
                        'message': result.stdout
                    })

    def cleanup_local(self, conn):
        # clean local package
        command = 'rm -rf {project_id}_{task_id}_*'.format(
            project_id=self.project.short_id, task_id=self.task.short_id
        )
        with conn.cd(self.local_codebase_project):
            conn.local(command)

    def cleanup_remote(self, conn):
        # clean remote package
        command = 'rm -rf {project_id}_{task_id}_*.tgz'.format(
            project_id=self.project.short_id, task_id=self.task.short_id
        )
        with conn.cd(self.project.target_releases):
            conn.run(command)

        # clean release version of obsolete
        self.send_message('info', {
            'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command='cd {}'.
                                                           format(self.project.target_releases))
        })
        with conn.cd(self.project.target_releases):
            command = 'ls -t ./| tail -n +{keep_version_num} | xargs rm -rf'.format(
                keep_version_num=int(self.project.keep_version_num) + 1
            )
            self.send_message('info', {
                'message': '[{user}@{host}]# {command}'.format(user=conn.user, host=conn.host, command=command)
            })
            result = conn.run(command)
            self.send_message('info', {
                'message': result.stdout
            })

    def deploy_start(self):
        self.stage = self.stage_start
        self.sequence = 1
        self.send_message(self.message_type_step)

        self.init_release()
        self.task.update(status=Task.STATUS_DOING)

    def deploy(self):
        self.deploy_start()

        is_all_servers_success = True
        try:
            local_conn = Connection(host='127.0.0.1', user='root', config=Deployer.config())
            self.prev_deploy(local_conn)
            self.checkout(local_conn)
            self.post_deploy(local_conn)

            # 建立SSHConnection缓存
            connections = []
            for server in self.servers:
                try:
                    connection = SSHConnection.new_connection(server, Deployer.config())
                    connections.append(connection)
                except socket.error as e:
                    current_app.logger.error(traceback.format_exc())
                    self.send_message('error', {
                        'message': '[{user}@{host}]# {command}'.format(user=server.username, host=server.manage_ip,
                                                                       command='- service connection timeout -')
                    })
                    continue

            # prev_release
            for remote_conn in connections:
                self.prev_release(remote_conn)

            # release
            for remote_conn in connections:
                self.release(remote_conn)

            # post_release
            for remote_conn in connections:
                self.post_release(remote_conn)

            # cleanup_remote
            for remote_conn in connections:
                self.cleanup_remote(remote_conn)

            # cleanup_local
            self.cleanup_local(local_conn)

            if len(connections) != len(self.servers):
                is_all_servers_success = False

        except Exception as e:
            is_all_servers_success = False
            self.send_message('error', {
                'message': '出现未知错误请联系管理员'
            })
            self.send_message('error', {
                'message': e.__repr__()
            })
            print(e)
            raise Exception(e)

        finally:
            self.deploy_end(is_all_servers_success)

    def deploy_end(self, success=True):
        if success:
            self.stage = self.stage_end
            self.sequence = 8
            self.send_message(self.message_type_step)

            self.task.update(
                link_id=self.release_version,
                pre_link_id=self.previous_release_version,
                status=Task.STATUS_SUCCESS
            )
        else:
            self.task.update(
                link_id=self.release_version,
                pre_link_id=self.previous_release_version,
                status=Task.STATUS_FAIL
            )

    def rollback_start(self):
        self.init_release(is_rollback=True)
        rollback_task = {
            'title': '{}-{}'.format(self.task.title, '回滚'),
            'project_id': self.task.project_id,
            'action': Task.ACTION_ROLLBACK,
            'status': Task.STATUS_NEW,
            'link_id': self.release_version,
            'create_by': current_user.name,
            'user_id': current_user.id
        }
        self.rollback_task = Task.create(**rollback_task)
        self.rollback_task.update(status=Task.STATUS_DOING)

    def rollback(self):
        self.rollback_start()

        is_all_servers_success = True
        try:
            # 建立SSHConnection缓存
            connections = []
            for server in self.servers:
                try:
                    connection = SSHConnection.new_connection(server, Deployer.config())
                    connections.append(connection)
                except socket.error as e:
                    current_app.logger.error(traceback.format_exc())
                    self.send_message('error', {
                        'message': '[{user}@{host}]# {command}'.format(user=server.username, host=server.manage_ip,
                                                                       command='- service connection timeout -')
                    })
                    continue

            # release
            for remote_conn in connections:
                self.release(remote_conn)

            if len(connections) != len(self.servers):
                is_all_servers_success = False

        except Exception as e:
            is_all_servers_success = False
            self.send_message('error', {
                'message': '出现未知错误请联系管理员'
            })
            raise Exception(e)
        finally:
            self.rollback_end(is_all_servers_success)

    def rollback_end(self, success=True):
        if success:
            self.stage = self.stage_end
            self.sequence = 8
            self.send_message(self.message_type_step)

            self.rollback_task.update(
                link_id=self.release_version,
                pre_link_id=self.previous_release_version,
                status=Task.STATUS_SUCCESS
            )
            # 标记该工单已被回滚
            self.task.update(status=Task.STATUS_ROLLBACK)
        else:
            self.rollback_task.update(
                link_id=self.release_version,
                pre_link_id=self.previous_release_version,
                status=Task.STATUS_FAIL
            )

    # ===================== revision ================
    # SocketHandler

    def list_tag(self):
        self.repo.init(os.path.join(self.local_codebase_project, 'revision'), is_need_clone=True)
        return self.repo.tags()

    def list_branch(self):
        self.repo.init(os.path.join(self.local_codebase_project, 'revision'), is_need_clone=True)
        branch_info = {}
        branches = self.repo.branches()
        if not branches:
            commit_numbers = []
            commits = self.repo.commits()
            for commit in commits:
                commit_numbers.append(commit['id'])
            branch_info['trunk'] = commit_numbers
        else:
            for branch in branches:
                commit_numbers = []
                commits = self.repo.commits(branch)
                for commit in commits:
                    commit_numbers.append(commit['id'])
                branch_info[branch] = commit_numbers
        return branch_info
